/**
 * Licensed under the MIT License
 *
 * Copyright (c) 2010 Alexandre Croiseaux
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package marcel.media.sound
{
	import flash.events.*;
	import flash.filters.*;
	import flash.media.*;
	import flash.net.URLRequest;
	import marcel.events.SoundStreamEvent;
	import marcel.utils.Framer;
	
	/**
	* @eventType marcel.events.SoundStreamEvent.START
	*/
	[Event(name="onSoundStreamStart", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.SEEK
	*/
	[Event(name="onSoundStreamSeek", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.STOP
	*/
	[Event(name="onSoundStreamStop", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.END
	*/
	[Event(name="onSoundStreamEnd", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.LOOP
	*/
	[Event(name="onSoundStreamLoop", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.PLAYING
	*/
	[Event(name="onSoundStreamPlaying", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.ERROR
	*/
	[Event(name="onSoundStreamError", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.LOAD_COMPLETE
	*/
	[Event(name="onSoundStreamLoadComplete", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.LOADING
	*/
	[Event(name="onSoundStreamLoading", type="marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.PAUSE
	*/
	[Event(name = "onSoundStreamPause", type = "marcel.events.SoundStreamEvent")]
	/**
	* @eventType marcel.events.SoundStreamEvent.ID3
	*/
	[Event(name="onSoundStreamID3", type="marcel.events.SoundStreamEvent")]
	
	
	/**
	 * SoundStream class used to play external MP3 file.
	 * @author Alexandre Croiseaux
	 */
	public class SoundStream extends EventDispatcher
	{
		//--------------------------------------------------------------------------
		//
		//  Public vars
		//
		//--------------------------------------------------------------------------
		/**
		 * Indicates if the stream must restarted from the begining when the playhead reaches the end of the file
		 */
		public var loop:Boolean = false;
		/**
		 * Indicates a seek point to go when the MP3 starts. !!Use only for debug purpose!!
		 */
		public var debugStartTime:Number = 0;
		/**
		 * The URI of the mp3 file being played
		 */
		public var uri:String;
		
		
		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		private var _snd:Sound;
		private var _channel:SoundChannel;
		private var _framer:Framer;
		private var _loaded:Boolean;
		private var _paused:Boolean;
		
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		/**
		 * Constructs a new SoundStream instance
		 */
		public function SoundStream()
		{
			_snd = new Sound();
			_channel = new SoundChannel();
			_framer = new Framer(checkPlayHead);
		}
		
		
		//--------------------------------------------------------------------------
		//
		//  Public methods
		//
		//--------------------------------------------------------------------------
		/**
		 * Calls Sound.load() to the specified URI and starts loading the MP3 file, the MP3 starts playing when the buffer is full
		 * @param	bufferTime	The number of milliseconds to preload a streaming sound into a buffer before the sound starts to stream.
		 * @param	checkPolicyFile	Specifies whether the application should try to download a URL policy file from the loaded sound's server before beginning to load the sound.
		 * @param	mp3URI	the URI of the MP3 file to play
		 */
		public function load(mp3URI:String, bufferTime:uint = 1000, checkPolicyFile:Boolean = false):SoundChannel
		{
			stop();
			
			uri = mp3URI;
			_loaded = false;
			_paused = false;
			_framer.start();
			
			_snd = new Sound();
			_snd.addEventListener(IOErrorEvent.IO_ERROR, onError);
			_snd.addEventListener(Event.ID3, onID3);
			_snd.load(new URLRequest(uri), new SoundLoaderContext(bufferTime, checkPolicyFile));
			_channel = _snd.play(debugStartTime, 0);
			_channel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);
			return _channel;
		}
		
		/**
		 * Stops playing the current MP3
		 */
		public function stop():void
		{
			_framer.stop();
			_channel.stop();
			try { _snd.close(); }
			catch (e:Error) {}
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.STOP));
		}
		
		/**
		 * The duration of the MP3, estimated while the MP3 is loading, and accurate when the MP3 is fully loaded.
		 */
		public function get duration():Number
		{
			if (_loaded) return _snd.length;
			else if (getPrcLoaded() == 0) return 0;
			else
			{
				var estimatedLength:Number = _snd.length * getPrcLoaded();
				return isNaN(estimatedLength) ? 0 : estimatedLength;
			}
		}
		
		/**
		 * Get the MP3 percent played (range [0-1])
		 * @return the MP3 percent played (range [0-1])
		 */
		public function getPrcPlayed():Number
		{
			if (isNaN(_channel.position) || duration == 0) return 0;
			else return _channel.position / duration;
		}
		
		/**
		 * Get the MP3 percent loaded (range [0-1])
		 * @return the MP3 percent loaded (range [0-1])
		 */
		public function getPrcLoaded():Number
		{
			if (isNaN(_snd.bytesTotal) || isNaN(_snd.bytesLoaded)) return 0;
			else return _snd.bytesLoaded / _snd.bytesTotal;
		}
		
		/**
		 * Indicates the volume of the MP3 being played (range[0-1])
		 * @param	volume	the volume of the MP3 (range[0-1])
		 */
		public function set volume(volume:Number):void
		{
			var st:SoundTransform = _channel.soundTransform;
			st.volume = volume;
			_channel.soundTransform = st;
		}
		
		/**
		 * Get the volume of the MP3 being played (range[0-1])
		 * @return	volume of the MP3 being played (range[0-1])
		 */
		public function get volume():Number { return _channel.soundTransform.volume; }
		
		/**
		 * Seeks to the specified location, in milliseconds.
		 * @param offset        The position in milliseconds at which playback should seek.
		 */
		public function seek(offset:Number):void
		{
			var vol:Number = volume;
			_channel.stop();
			_channel = _snd.play(offset);
			volume = vol; // update channel soundTransform
			_channel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.SEEK, offset));
		}
		
		/**
		 * Pauses playback of a MP3 stream.
		 */
		public function pause():void
		{
			_paused = true;
			_channel.stop();
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.PAUSE, _paused));
		}
		
		/**
		 * Resumes playback of a MP3 stream that is paused.
		 */
		public function resume():void
		{
			var vol:Number = volume;
			_paused = false;
			_channel = _snd.play(_channel.position);
			volume = vol; // update channel soundTransform
			_channel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.PAUSE, _paused));
		}
		
		/**
		 * Pauses playback of a MP3 stream that is playing or resumes playback of a MP3 stream that is paused.
		 */
		public function togglePause():void
		{
			_paused ? resume() : pause();
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.PAUSE, _paused));
		}
		
		/**
		 * Returns the inner Sound object
		 * @return	the Sound instance used to display the stream
		 */
		public function getSound():Sound { return _snd; }
		
		/**
		 * Returns the inner SoundChannel object
		 * @return	the SoundChannel instance used to display the stream
		 */
		public function getSoundChannel():SoundChannel { return _channel; }
		
		/**
		 * The position of the playhead, in seconds.
		 * @return	The position of the playhead, in seconds.
		 */
		public function getTime():Number { return _channel.position; }
		
		/**
		 * Checks is the mp3 is paused or not
		 * @return	true if the mp3 stream is paused, false otherwise
		 */
		public function isPaused():Boolean { return _paused; }
		
		/**
		 * Checks is the mp3 is loaded or not
		 * @return	true if the mp3 stream is loaded, false otherwise
		 */
		public function isLoaded():Boolean { return _loaded; }
		
		
		//--------------------------------------------------------------------------
		//
		//  Private methods
		//
		//--------------------------------------------------------------------------
		private function checkPlayHead():void
		{
			if (!isPaused()) dispatchEvent(new SoundStreamEvent(SoundStreamEvent.PLAYING, getPrcPlayed()));
			if (!_loaded) dispatchEvent(new SoundStreamEvent(SoundStreamEvent.LOADING, getPrcLoaded()));
			
			if (getPrcLoaded() >= 1 && !_loaded)
			{
				_loaded = true;
				dispatchEvent(new SoundStreamEvent(SoundStreamEvent.LOAD_COMPLETE));
			}
		}
		
		//--------------------------------------------------------------------------
		//
		//  Events
		//
		//--------------------------------------------------------------------------
		private function onError(evt:IOErrorEvent):void
		{
			stop();
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.ERROR, evt.text));
		}
		
		private function onSoundComplete(evt:Event):void
		{
			if (loop)
			{
				seek(0);
				dispatchEvent(new SoundStreamEvent(SoundStreamEvent.LOOP));
			}
			else dispatchEvent(new SoundStreamEvent(SoundStreamEvent.END));
		}
		
		private function onID3(evt:Event):void
		{
			dispatchEvent(new SoundStreamEvent(SoundStreamEvent.ID3, _snd.id3));
		}
	}
}